Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Rendered β€’ View raw β€’ Download

workpad.md 3fe1deb01ccbc17b2d08941cd3dfed7b747dd325 (3fe1deb0) Text, 9.63 KB

Craft Workpad: Live BT/Wi-Fi adapter-state detection

β”‚ Generated by /craft Β· Started: 2026-06-18 Β· Branch: claude/gallant-thompson-d1b2ea (PR #5851)


Task

Make the Bluetooth/Wi-Fi adapter-disabled detection live instead of T383838ON_RESUME-polled β€” add a T383838BroadcastReceiver for Bluetooth adapter state and a T383838ConnectivityManager.NetworkCallback for network availability, so the Connections recovery banners update in real time (e.g. toggling BT from the quick-settings shade) rather than only when the activity resumes.

Scope: T383838isBluetoothDisabled() and T383838isWifiUnavailable() in T383838core/ui/src/androidMain/.../util/PlatformUtils.kt. Follow-up to the adapter-state feature added in commit 2c06a8019 on PR #5851.


Exploration Report

Key Facts

β€’ Both target functions live in T383838core/ui/src/androidMain/.../util/PlatformUtils.kt: T383838isBluetoothDisabled() (reads T383838BluetoothManager.adapter.isEnabled) and T383838isWifiUnavailable() (reads T383838ConnectivityManager.activeNetwork transports). Both currently wrap their read in the private T383838rememberOnResumeState { ... } helper β€” recomputed only on T383838Lifecycle.Event.ON_RESUME.
β€’ T383838rememberOnResumeState(check) is the only consumer-shared refresh primitive; also used by T383838isGpsDisabled(). Changing the two BT/Wi-Fi functions must NOT change T383838isGpsDisabled() behavior (out of scope).
β€’ NetworkCallback pattern already exists in T383838core/network/.../ConnectivityManager.kt: T383838observeNetworks() uses T383838callbackFlow { … registerNetworkCallback(req, cb); awaitClose { unregisterNetworkCallback(cb) } } with T383838onAvailable/T383838onLost/T383838onCapabilitiesChanged. Mirror it with a T383838DisposableEffect in the composable (or T383838registerDefaultNetworkCallback for the single active network).
β€’ BroadcastReceiver pattern already exists in T383838core/ble/.../AndroidBluetoothRepository.kt: registers via T383838ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) and T383838context.unregisterReceiver(...). For adapter state, the action is T383838BluetoothAdapter.ACTION_STATE_CHANGED.
β€’ The two functions are consumed only by T383838ConnectionsScreen.kt (hoisted as T383838bluetoothDisabled/T383838wifiUnavailable booleans driving inline T383838RecoveryCard banners + the BLE toggle routing). No other callers β€” the public contract (T383838@Composable expect fun … : Boolean) is unchanged; only the androidMain implementation changes.
β€’ T383838androidApp/src/main/AndroidManifest.xml already declares Bluetooth + network permissions; T383838ACTION_STATE_CHANGED and a T383838NetworkCallback need no extra permission beyond what's declared (T383838ACCESS_NETWORK_STATE is present for connectivity callbacks β€” verify).

Key Files

β€’ T383838core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt β€” the two functions + T383838rememberOnResumeState; the only file that changes.
β€’ T383838core/network/.../repository/ConnectivityManager.kt β€” reference NetworkCallback/callbackFlow pattern.
β€’ T383838core/ble/.../AndroidBluetoothRepository.kt β€” reference registerReceiver/RECEIVERNOTEXPORTED pattern.
β€’ T383838feature/connections/.../ui/ConnectionsScreen.kt β€” sole consumer (no change needed; reads the same booleans, now updated live).
β€’ jvm/ios actuals already return constant T383838false β€” unaffected.


Requirements

R1: T383838isBluetoothDisabled() updates reactively β€” when the Bluetooth adapter is turned on/off (incl. from the quick-settings shade while the app is foregrounded), the returned value changes without waiting for T383838ON_RESUME.
Source: human confirmed
Verification: review confirms a T383838BroadcastReceiver on T383838BluetoothAdapter.ACTION_STATE_CHANGED drives the state; manual toggle from shade flips the banner.

R2: T383838isWifiUnavailable() updates reactively β€” connecting/disconnecting Wi-Fi (or losing the active local network) changes the returned value live.
Source: human confirmed
Verification: review confirms a T383838ConnectivityManager T383838NetworkCallback (default-network) drives the state.

R3: T383838ON_RESUME polling is removed for these two functions; the receiver/callback is registered and unregistered with the composable's lifetime via T383838DisposableEffect (no leaks). T383838isGpsDisabled() continues to use T383838rememberOnResumeState unchanged.
Source: human confirmed
Verification: T383838grep shows no T383838rememberOnResumeState in the two functions; T383838awaitClose/T383838onDispose unregisters; T383838isGpsDisabled untouched.

R4: Registration follows existing repo conventions β€” T383838ContextCompat.registerReceiver(..., RECEIVER_NOT_EXPORTED) for the BT receiver; T383838registerDefaultNetworkCallback/T383838unregisterNetworkCallback for the network callback. No new permissions (ACCESSNETWORKSTATE already declared).
Source: craft-clarify recommendation
Verification: review confirms the registration calls + flag matches T383838RECEIVER_NOT_EXPORTED.

R5: The public T383838expect contract and all consumers are unchanged β€” T383838ConnectionsScreen reads the same T383838Booleans, now live. jvm/ios actuals stay constant T383838false.
Source: craft-clarify recommendation
Verification: no change to commonMain expect or jvm/iosMain; ConnectionsScreen diff empty; both flavors assemble.


Architectural Decision

β€’ Chosen approach: Extract a private T383838rememberObservedFlag(read, subscribe) primitive (DisposableEffect + mutableStateOf, re-seed on registration); express T383838isBluetoothDisabled() via a T383838BroadcastReceiver on T383838ACTION_STATE_CHANGED (RECEIVERNOTEXPORTED, main-thread delivery) and T383838isWifiUnavailable() via T383838registerDefaultNetworkCallback(callback, mainHandler).
β€’ Approved: 2026-06-18
β€’ Adversarial: P1-ish threading risk (NetworkCallback on background thread) mitigated by main-thread Handler; leaks prevented by onDispose unregister; cold-start staleness prevented by read() re-seed. No blocker.
β€’ Files: T383838core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt only.
β€’ Test: build both flavors + detekt/spotless; manual shade-toggle verification (no headless test feasible β€” T383838core/ui has no instrumentation).


Acceptance Criteria

AC1: [R1] T383838isBluetoothDisabled() is driven by a T383838BroadcastReceiver on T383838BluetoothAdapter.ACTION_STATE_CHANGED. Auto-verify: grep ACTIONSTATECHANGED + BroadcastReceiver in the function.
AC2: [R2] T383838isWifiUnavailable() is driven by T383838registerDefaultNetworkCallback. Auto-verify: grep registerDefaultNetworkCallback.
AC3: [R3] neither function uses T383838rememberOnResumeState; observer torn down in T383838onDispose; T383838isGpsDisabled() still uses T383838rememberOnResumeState. Auto-verify: grep.
AC4: [R4] BT receiver uses T383838RECEIVER_NOT_EXPORTED; network callback uses a main-thread T383838Handler. Auto-verify: grep.
AC5: [R5] commonMain expect + jvm/ios actuals + ConnectionsScreen unchanged; both flavors assemble; detekt/spotless clean. Auto-verify: git diff scope + build.


Implementation Plan

Branch: claude/gallant-thompson-d1b2ea Base commit: 2c06a8019

Task list

Pre-existing failures (do not fix β€” out of scope)


Completion Bar

1. [x] All planned files created/modified
2. [x] Linter clean
3. [x] Tests pass (no new tests feasible; regressions green)
4. [x] Every AC has a completion note
5. [x] No open markers remain
6. [x] Scope discipline honored


Review

Reviewer: concurrency (right-sized β€” single-file ~40-line change on established repo patterns; build+detekt covered the rest).

Verdict: SOUND. Threading main-thread-confined (BT receiver no-Handler β†’ main; NetworkCallback with main-Looper Handler); register/unregister balanced 1:1 via T383838DisposableEffect(Unit)/T383838onDispose; no leak or double-unregister; T383838read() re-seed correct.

Findings (both P3, no action):
β€’ F-1 [P3] T383838rememberUpdatedState(subscribe) freshness is never exercised (subscribe runs once). NOT removed: accessing T383838subscribe directly inside T383838DisposableEffect re-triggers the T383838LambdaParameterInRestartableEffect detekt rule, so the wrapper is required for lint. T383838LocalContext is stable, so no stale-context defect. Kept as-is.
β€’ F-2 [P3] T383838registerDefaultNetworkCallback T383838TooManyRequests β€” structurally bounded (1:1 with a single live banner); no retry loop. No guard needed.

Acceptance criteria status
β€’ AC1 βœ“ BroadcastReceiver on ACTIONSTATECHANGED drives isBluetoothDisabled.
β€’ AC2 βœ“ registerDefaultNetworkCallback drives isWifiUnavailable.
β€’ AC3 βœ“ neither uses rememberOnResumeState; onDispose unregisters; isGpsDisabled untouched.
β€’ AC4 βœ“ RECEIVERNOTEXPORTED + main-thread Handler.
β€’ AC5 βœ“ expect/jvm/ios/ConnectionsScreen unchanged; both flavors assemble; detekt/spotless clean.


Deferred Items


Phase Log

β€’ explore: done β€” 2026-06-18 β€” lean direct explore (deep prior context). Both reactive patterns already in repo (NetworkCallback callbackFlow in core/network; registerReceiver RECEIVERNOTEXPORTED in core/ble). Only androidMain PlatformUtils changes.
β€’ clarify: done β€” 2026-06-18 β€” Q1 confirmed (replace ON_RESUME entirely). R1–R5 recorded.
β€’ architect: done β€” 2026-06-18 β€” single approach (rememberObservedFlag primitive) + self-adversarial pass (threading via main Handler). Approved.
β€’ implement: done β€” 2026-06-18 β€” one file (core/ui androidMain PlatformUtils). All 5 ACs met; build+detekt+spotless+both flavors green.
β€’ review: done β€” 2026-06-18 β€” concurrency reviewer: SOUND. 2Γ—P3 non-actionable (lint-required wrapper; bounded TooManyRequests).
β€’ refine: done β€” 2026-06-18 β€” fast path, no actionable findings. P3s recorded as considered/declined.
β€’ pr: done β€” 2026-06-18 β€” pushed to existing PR #5851 (174db32ae); no new PR (same branch/feature).

Served by rngit 1.5.2 - Generated in 0.03s